﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using TCPSockets.ClientEvents.Args;
using TCPSockets.ClientEvents.Handlers;
using TCPSockets.Events.Args;
using TCPSockets.Interfaces;

namespace TCPSockets
{
    /// <summary>
    /// Класс, реализующий клиентскую программу для TCP соединения
    /// </summary>
    public class CTCPClient : IDisposable, IClient
    {
        /* - Описание событий, делегатов и классов аркументов, возращаемых событиями --------------------------------------------------------- */

        #region События

        /// <summary>
        /// Событие соединения клиента с сервером
        /// </summary>
        public event OnConnectedEventHandler OnConnected;

        /// <summary>
        /// Позволяет легко вызвать событие "при присоединении"
        /// </summary>
        /// <remarks>
        /// Метод для технического использования
        /// В интерфейс класса не входит.
        /// </remarks>
        protected virtual void InvokeConnectedEvent()
        {
            if(OnConnected != null)
                OnConnected.BeginInvoke(this, new EventArgs(), null, null);
        }

        /// <summary>
        /// Событие, возникающие при потери связи
        /// </summary>
        public event OnDisconnectedEventHandler OnDisconnected;

        /// <summary>
        /// Позволяет легко вызвать событие "при отсоединении"
        /// </summary>
        /// <remarks>
        /// Метод для технического использования
        /// В интерфейс класса не входит.
        /// </remarks>
        protected virtual void InvokeDisconnectedEvent()
        {
            if(OnDisconnected != null)
                OnDisconnected.BeginInvoke(this, new EventArgs(), null, null);
        }

        /// <summary>
        /// Событие, возникающие при ошибке
        /// </summary>
        public event OnErrorEventHandler OnError;

        /// <summary>
        /// Позволяет легко вызвать событие "при ошибке"
        /// </summary>
        /// <remarks>
        /// Метод для технического использования
        /// В интерфейс класса не входит.
        /// </remarks>
        /// <param name="Error">Возникшая ошибка</param>
        protected virtual void InvokeErrorEvent(Exception Error)
        {
            if(OnError != null)
                OnError.BeginInvoke(this, new OnErrorEventArgs(Error), null, null);
            else
                throw Error;
        }

        /// <summary>
        /// Событие, возникающие при появлении данных в сетевом потоке
        /// </summary>
        public event OnReceiveDataEventHandler OnReceiveData;

        /// <summary>
        /// Позволяет легко вызвать событие "при получении данных" 
        /// с параметром в виде самих данных
        /// </summary>
        /// <remarks>
        /// Метод для технического использования
        /// В интерфейс класса не входит.
        /// </remarks>
        /// <param name="Data">Полученные данные</param>
        protected virtual void InvokeReceiveDataEvent(byte[] Data)
        {
            if(OnReceiveData != null)
                OnReceiveData.BeginInvoke(this, new DataEventArgs(Data, fDataEncoding, fDataFormatter),
                    null, null);
        }

        /// <summary>
        /// Событие, возникающие при благополучной отправке
        /// </summary>
        public event OnDataSendEventHandler OnDataSend;

        protected virtual void InvokeDataSendEvent(byte[] Data) 
        {
            if(OnDataSend != null)
                OnDataSend.BeginInvoke(this, new DataEventArgs(Data, fDataEncoding, fDataFormatter),
                    null, null);
        }

        #endregion

        /* - Атрибуты клиента ---------------------------------------------------------------------------------------------------------------- */

        #region Атрибуты

        /// <summary>
        /// Основной объект клиента
        /// </summary>
        /// <remarks>
        /// Инкапсулирует работу с сетевым потоком данных и с сокетами
        /// В интерфейс класса не входит
        /// </remarks>
        protected TcpClient fClient;

        /// <summary>
        /// Сетевой поток данных
        /// </summary>
        protected NetworkStream ClientStream;
        /// <summary>
        /// Объект для чтения из сетевого потока данных
        /// </summary>
        /// <remarks>В интерфейс класса не входит</remarks>
        protected BinaryReader bReader;
        /// <summary>
        /// Объект для записи данных в поток
        /// </summary>
        /// <remarks>В интерфейс класса не входит</remarks>
        protected BinaryWriter bWriter;

        protected Encoding fDataEncoding = Encoding.ASCII;

        /// <summary>
        /// Строка с именем удалённого Host'а
        /// </summary>
        /// <remarks>В интерфейс класса не входит</remarks>
        protected string fHost = "127.0.0.1";
        /// <summary>
        /// Переменная целого типа, указывающай удалённый порт
        /// </summary>
        /// <remarks>В интерфейс класса не входит</remarks>
        protected int fPort = 8080;
        /// <summary>
        /// главноый поток клиента. В нём происходит обработка поступающих данных и возникают события "при получении данных" и "при потери связи". Так же в нём проводится проверка на подключение к удалённому серверу.
        /// </summary>
        /// <remarks>
        /// Для внутриклассового использования.
        /// В интерфейс класса не входит
        /// </remarks>
        protected Thread fCheckThread = null;
        protected int fSleepTime = 100;
        /// <summary>
        /// Bool'евская переменная, отражающая статус клиента - подключён / не подключён
        /// </summary>
        /// <remarks>В интерфейс класса не входит</remarks>
        protected bool fConnected = false;

        protected IFormatter fDataFormatter = new BinaryFormatter();

        #endregion

        /* - Свойства ------------------------------------------------------------------------------------------------------------------------ */

        #region Свойства

        /// <summary>
        /// Свойство клиента, отражающее его статус
        /// </summary>
        /// <value>Подключён / не подключён</value>
        public bool Connected
        {
            get { return fConnected; }
            set { if(value) Start(); else Stop(); }
        }

        /// <summary>
        /// Удалённый Host
        /// </summary>
        /// <remarks>Можно изменять во время работы слиента. Клиент отключится от сервера, изменит настройку и попытается переподключиться к указанному Host'у</remarks>
        /// <value>Имеет строковый тип. Может быть указан в виде IP адреса.</value>
        public string Host
        {
            get { return fHost; }
            set
            {
                //Если пытаемся установить уже текущий Host, то возврат
                if(value == fHost) return;
                //сохраняем текущее состояние клиента
                bool Temp = Connected;
                //Отключаемся
                Connected = false;
                //Присваеваем значение
                fHost = value;
                //Восстанавливаем состояние
                Connected = Temp;
            }
        }

        /// <summary>
        /// Удалённый порт
        /// </summary>
        /// <remarks>
        /// В случае задания недопустимого значения будет сгенерировано исключение
        /// Может быть изменино во время работы клиента. Клиент отключиться от сервера, изменит настройку и попытается установить соединение с новым сервером.
        /// </remarks>
        /// <value>принимает значения целого типа в пределах от 1 до 2^16</value>
        public int Port
        {
            get { return fPort; }
            set
            {
                //Если пытаемся установить текущее значение порта, то возврат
                if(value == fPort) return;
                //Если значение недопустимо, то генерируем исключение
                if(value < 1 || value > 65535)
                    throw new ArgumentOutOfRangeException("Port", value, "Порт должен быть в пределах от 1 до 65535, а указан " + value.ToString());

                //Сохраняем текущее соятояние
                bool Temp = Connected;
                //Отключяемся
                Connected = false;
                //Присваеваем новое значение порта
                fPort = value;
                //Восстанавливаем исходное состояние
                Connected = Temp;
            }
        }

        public Encoding DataEncoding { get { return fDataEncoding; } set { fDataEncoding = value; } }
        public NetworkStream SocketStream { get { return ClientStream; } }
        public IFormatter DataFormatter
        {
            get { return fDataFormatter; }
            set
            {
                if(value == null) throw new NullReferenceException("Не задан сериализатор!");
                fDataFormatter = value;
            }
        }

        #endregion

        /* - Время жизни объекта (конструктор / диструктор) ---------------------------------------------------------------------------------- */

        #region Конструктор / диструктор

        /// <summary>
        /// Конструктор без параметров.
        /// </summary>
        /// <remarks>
        /// предпологается, что все необходимые параметры будут заданы во время работы с созданным объектом.
        /// Изначальные значения
        /// Host = "127.0.0.1"
        /// Port = 8080
        /// </remarks>
        public CTCPClient()
        {

        }
        
        public CTCPClient(string Host)
        {
            string[] Data = Host.Split(':');
            fHost = Data[0];
            if(Data.Length > 1)
                if(!int.TryParse(Data[1], out fPort))
                    throw new ArgumentException("Неверный формат строки. Должен быть: \"Host\", или \"Host\":Port");

        }

        /// <summary>
        /// Конструктор клиента по указанным Host'у и порту
        /// </summary>
        /// <param name="Host">Адрес удалённого сервера</param>
        /// <param name="Port">Порт</param>
        public CTCPClient(string Host, int Port)
        {
            //Если значение порта недопустимо, то генерируем исключительную ситуацию
            if(Port < 1 || Port > 65535)
                throw new ArgumentOutOfRangeException(nameof(Port), Port, "Порт должен быть в пределах от 1 до 65535, а указан " + Port.ToString());
            fHost = Host;
            fPort = Port;
        }

        /// <summary>
        /// Конструктор клиента по указанному адресу сервера, порту и состоянию клиента после его создания.
        /// </summary>
        /// <param name="Host">Адрес удалённого сервера</param>
        /// <param name="Port">Порт</param>
        /// <param name="Enabel">Состояние подключения после создания. True - клиент будет запущен непосредственно по завершении работы конструктора</param>
        public CTCPClient(string Host, int Port, bool Enabel)
        {
            //Если значение порта недопустимо, то генерируем исключительную ситуацию
            if(Port < 1 || Port > 65535)
                throw new ArgumentOutOfRangeException(nameof(Port), Port, "Порт должен быть в пределах от 1 до 65535, а указан " + Port.ToString());
            fHost = Host;
            fPort = Port;
            if(Enabel) Connected = true;
        }

        /// <summary>
        /// Конструктор клиена по указанному объекту типа TcpLient
        /// </summary>
        /// <remarks>Нужен для использования в серверной части</remarks>
        /// <param name="Client">Объект класса TcpClient</param>
        public CTCPClient(TcpClient Client)
        {
            fClient = Client;
            fConnected = true;
            //проверяем, получен ли от сервера подключённый клиент. Если соединение отсутствует, то возврат
            if(!Connected) return;

            //Создаём сетевой поток
            ClientStream = new NetworkStream(fClient.Client);
            //ClientStream.ReadTimeout = 250;
            //...объект для чтения данных из потока
            bReader = new BinaryReader(ClientStream);
            //...объект для записи данных в поток
            bWriter = new BinaryWriter(ClientStream);

            //Выясняем удалённый адрес Host'а
            fHost = fClient.Client.RemoteEndPoint.ToString().Split(':')[0];
            //Выясняем порт удалённого клиента
            fPort = Int32.Parse(fClient.Client.RemoteEndPoint.ToString().Split(':')[1]);

            //Создаём основной поток клиента
            fCheckThread = new Thread(new ThreadStart(CheckConnection));
            //Запускаем основной поток клиента
            fCheckThread.Start();
            //Сообщаем о подключении
            InvokeConnectedEvent();
        }

        #endregion

        /* - Методы запуска / остановки ------------------------------------------------------------------------------------------------------ */

        #region Запуск / остановка

        /// <summary>
        /// Метод установления соединения у удалённым сервером
        /// </summary>
        /// <param name="Host">Адрес удалённого сервера</param>
        /// <param name="Port">Порт</param>
        public bool Connect(string Host, int Port)
        {
            //Если подключение к указанному адресу и порту уже выполнено, то возврат
            if(Connected && Host == fHost && Port == fPort) return true;

            //В случае, если клиент был подключён, отключаемся
            Connected = false;

            //Устанавливаем новые параметры соединения
            this.Host = Host;
            this.Port = Port;

            //Подключаемся к новой точке
            Connected = true;

            return Connected;
        }

        /// <summary>
        /// Метод запуска клиента
        /// </summary>
        /// <remarks>
        /// Метод для технического использования.
        /// В интерфейс класса не входит
        /// </remarks>
        protected void Start()
        {
            //Если соединение уже установлено, то возврат
            if(Connected) return;

            try
            {
                //Устанавливаем флаг, свидетельствющий о наличии соединения
                fConnected = true;

                //Создаём нового клиента
                if(fClient == null) fClient = new TcpClient();
                //Подключаем клиента к удалённому серверу
                fClient.Connect(fHost, fPort);

                //Получаем сетевой поток
                ClientStream = fClient.GetStream();
                //Создаём объект для чтения из сетевого потока
                bReader = new BinaryReader(ClientStream);
                //...и для записи в сетевой поток
                bWriter = new BinaryWriter(ClientStream);

                //Создаём основной поток клиента
                fCheckThread = new Thread(new ThreadStart(CheckConnection));
                //Запускаем основной поток клиента
                fCheckThread.Start();

                //сообщаем о подключении
                InvokeConnectedEvent();
            }
            //В случае возникновения ошибки сокета
            catch(SocketException SocketError)
            {
                //Сбрасываем флаг подключения
                fConnected = false;
                //Делаем непонятные телодвижения в плане анализ кодов ошибок... зачем - непонятно...
                if(SocketError.ErrorCode == 10061)
                {
                    InvokeDisconnectedEvent();
                    InvokeErrorEvent(SocketError);
                }
                else
                {
                    //Сообщаем о появлении ошибки
                    InvokeErrorEvent(SocketError);
                }
            }
        }

        /// <summary>
        /// Метод остановки клиента
        /// </summary>
        /// <remarks>
        /// Метод для технического использования.
        /// В интерфейс класса не входит.
        /// </remarks>
        protected void Stop()
        {
            //Если связь не была установлена, то возврат
            if(!Connected) return;
            //Сбрасываем флаг подключения
            fConnected = false;

            //Начинаем асинхронный процесс отключения
            fClient.Client.BeginDisconnect(false, new AsyncCallback(DisconnectionComplite), fClient.Client);
        }

        /// <summary>
        /// Метод асинхронного завершения процесса отключения от сервера
        /// </summary>
        protected void DisconnectionComplite(IAsyncResult AsyncResult)
        {
            //Получаем сокет клиента
            Socket ClientSocket = (Socket)AsyncResult.AsyncState;
            try
            {
                //Пытаемся его отключить
                ClientSocket.EndDisconnect(AsyncResult);

                //Если главный поток клиента ещё активен
                if(fCheckThread != null)
                {
                    bReader.Close();
                    if(fCheckThread.IsAlive)
                        fCheckThread.Abort(); //Останавливаем поток
                    fCheckThread.Join(); //Ожидаем завершения
                    fCheckThread = null; //Убираем ссылку на главный поток (отдаём его на растерзание сборщику мусора)
                }

                //Закрываем поток данных
                ClientStream.Close();

                bReader = null;
                bWriter = null;

                ClientStream = null;

                //Убираем ссылку на клиента
                fClient = null;

                //Сообщаем об отключении
                InvokeDisconnectedEvent();

            }
            //В случае ошибки
            catch(Exception Error)
            {
                //Сообщаем о ошибке
                InvokeErrorEvent(Error);
            }
        }

        #endregion

        /* - Отправка / получение данных и проверка соединения ------------------------------------------------------------------------------- */

        #region Отправка / получение

        /// <summary>
        /// Главный метод клиента. Стартует в отдельном потоке. Проверяет наличие данных от сервера и связь с ним.
        /// </summary>
        protected void CheckConnection()
        {
            //Читаем из потока до тех пор, пока не будет сброшен флаг подключения (сверху не решат отключиться)
            //или пока не будет получено "ничто" - это происходит, когда удалённый сервер порвал соединение
            try
            {

                byte[] ReceiveBuffer = new byte[1];
                bool b = true;
                while(Connected && (ReceiveBuffer.Length != 0 || b))
                {
                    ReceiveBuffer = bReader.ReadBytes(fClient.Available);
                    b = fClient.Available != 0;
                    //Если что-то получино, и это что-то имеет длину, большую 0-я, то сообщаем об этом
                    if(ReceiveBuffer != null)
                        if(ReceiveBuffer.Length > 0)
                            InvokeReceiveDataEvent(ReceiveBuffer);

                }
            }
            catch(ThreadAbortException)
            { }
            catch(IOException)
            { }

            //После выпадения из цыкла (значит соединение было разорвано) вызываем метод отключения клиента.
            //Просто так его вызвать нельзя потому, что тогда поток будет ждатье го завершшения, а в это метода
            //происходит остановка этого потока с его синхронизацией. В итоге происходит мёртвая блокировка.
            //Во избежании этого мы запускаем метод отключения в отдельном потоке, а этот благополучно завершается.
            new Thread(new ThreadStart(Stop)).Start();

            //В случае если соединение рвётся на нашей стороне по команде пользователя, то метод отключения уже инициирован, 
            //но мы его запускаем ещё раз. Это делать можно потому, что поток будет остановлен тем мтодом в момент
            //когда уже будет сброшен флаг активности клиента и это не позволит повторно войти в метод остановки.
        }

        /* ----------------------------------------------------------------------------------------------------------------------------------- */

        /// <summary>
        /// Метод отправки данных на сервер
        /// </summary>
        /// <param name="Message">Отправляемые данные</param>
        public void Send(string Message)
        {
            Send(DataEncoding.GetBytes(Message));
        }

        public void Send(byte[] Data)
        {
            if(!Connected) return;
            try
            {
                lock(ClientStream)
                {
                    bWriter.Write(Data);
                    bWriter.Flush();
                }
                InvokeDataSendEvent(Data);
            }
            catch(SocketException SocketError)
            {
                if(SocketError.ErrorCode == 10053)
                {
                    Stop();
                    InvokeDisconnectedEvent();
                }
                else
                {
                    Stop();
                    InvokeErrorEvent(SocketError);
                }
            }
            catch(IOException)
            {
                Stop();
                InvokeDisconnectedEvent();
            }
            catch(Exception Error)
            {
                InvokeErrorEvent(Error);
            }
        }

        public void SendObject(object Object)
        {
            lock(ClientStream) DataFormatter.Serialize(ClientStream, Object);
        }

        #endregion

        /* ----------------------------------------------------------------------------------------------------------------------------------- */

        public override string ToString() { return Host + ":" + Port.ToString(); }

        public static explicit operator TcpClient(CTCPClient Client)
        {
            return Client.fClient;
        }

        public static explicit operator CTCPClient(TcpClient Client)
        {
            return new CTCPClient(Client); 
        }

        #region IDisposable Members

        public void Dispose()
        {
            Stop();
        }

        void IDisposable.Dispose()
        {
            Dispose();
        }

        #endregion

        #region IClient Members

        string IClient.Host
        {
            get
            {
                return Host;
            }
            set
            {
                Host = value;
            }
        }

        int IClient.Port
        {
            get
            {
                return Port;
            }
            set
            {
                Port = value;
            }
        }

        bool IClient.Connected
        {
            get
            {
                return Connected;
            }
            set
            {
                Connected = value;
            }
        }

        bool IClient.Connect(string Host, int Port)
        {
            return Connect(Host, Port);
        }

        void IClient.Send(string Message)
        {
            Send(Message);
        }

        event OnConnectedEventHandler IClient.OnConnected
        {
            add { OnConnected += value; }
            remove { OnConnected -= value; }
        }
        event OnDataSendEventHandler IClient.OnDataSend
        {
            add { OnDataSend += value; }
            remove { OnDataSend -= value; }
        }
        event OnDisconnectedEventHandler IClient.OnDisconnected
        {
            add { OnDisconnected += value; }
            remove { OnDisconnected -= value; }
        }
        event OnErrorEventHandler IClient.OnError
        {
            add { OnError += value; }
            remove { OnError -= value; }
        }
        event OnReceiveDataEventHandler IClient.OnReceiveData
        {
            add { OnReceiveData += value; }
            remove { OnReceiveData -= value; }
        }

        #endregion
    }
}
